package widget.drag;

import android.annotation.TargetApi;
import android.content.Context;
import android.graphics.Rect;
import android.os.Build;
import android.support.annotation.Nullable;
import android.support.v4.view.ViewCompat;
import android.support.v4.widget.ViewDragHelper;
import android.util.AttributeSet;
import android.view.View;
import android.view.ViewConfiguration;
import android.view.ViewGroup;
import android.view.ViewTreeObserver;
import android.view.accessibility.AccessibilityEvent;
import android.widget.LinearLayout;

import java.util.ArrayList;
import java.util.List;

import library.utils.DisplayUtil;


/**
 * Created by apple on 2017/4/13.
 */

public abstract class DragView extends LinearLayout {

    // ViewDragHelper 的敏感度
    public static final float TOUCH_SLOP_SENSITIVITY = 1.0f;
    public static final int MIN_FLING_VELOCITY = 300; // dips per second

    private int mMaxFlingVelocity;

    private ViewDragHelper mDragHelper;

    private final List<OnScrollChangeListener> mOnScrollChangeListeners = new ArrayList<>();
    private final List<OnStateChangedListener> mOnStateChangedListeners = new ArrayList<>();
    private DragLayout mParent;
    private DragRange mDragRange;
    private float percent = -1.F;

    private List<View> mScrollableViews = new ArrayList<>(1);

    private int mLastTop = -1;
    private Integer mSmoothTo = -1;
    private boolean isPaused = false;

    public DragView(Context context) {
        super(context);
        _init(context);
    }

    public DragView(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
        _init(context);
    }

    public DragView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        _init(context);
    }

    @TargetApi(Build.VERSION_CODES.LOLLIPOP)
    public DragView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
        super(context, attrs, defStyleAttr, defStyleRes);
        _init(context);
    }

    private void _init(Context context) {
        setClickable(true);
        setFocusable(false);
        setFocusableInTouchMode(false);
        ViewConfiguration configuration = ViewConfiguration.get(context);
        mMaxFlingVelocity = configuration.getScaledMaximumFlingVelocity();

        getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
            @Override
            public void onGlobalLayout() {
                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
                    getViewTreeObserver().removeOnGlobalLayoutListener(this);
                } else {
                    getViewTreeObserver().removeGlobalOnLayoutListener(this);
                }
                mParent = (DragLayout) getParent();
                Rect frame = new Rect();
                getRootView().getWindowVisibleDisplayFrame(frame);
                int top = frame.bottom > frame.top ? Math.max(mParent.getTop(), frame.top) : mParent.getTop();
                int bottom = frame.bottom > frame.top ? Math.min(mParent.getBottom(), frame.bottom) : mParent.getBottom();
                mDragRange = getDragRange();
                if (mDragRange == null) {
                    mDragRange = new DragRange(top, bottom);
                } else {
                    if (mDragRange.top < 0)
                        mDragRange.top = top;
                    if (mDragRange.bottom < 0)
                        mDragRange.bottom = bottom;
                }

                requestLayout();

                getViewDragHelper();
                if (isAutoShow()) {
                    post(new Runnable() {
                        @Override
                        public void run() {
                            show();
                        }
                    });
                }
            }
        });

    }


    /* ABS start */
    protected abstract int getMinHeight();

    protected abstract DragRange getDragRange();

    protected abstract boolean onDragRelease(float percent, float direction);

    /* ABS end */


    /* Override start */
    protected boolean isAutoShow() {
        return false;
    }

    protected boolean isAllowDrag() {
        return true;
    }

    protected boolean isAllowTouchOutSide() {
        return false;
    }
    /* Override end */


    private DragView getView() {
        return this;
    }

    private DragLayout getParentView() {
        return (DragLayout) getParent();
    }

    protected int getTopLimit() {
        try {
            return mDragRange != null && mDragRange.top > 0 ? Math.max(mParent.getTop(), mDragRange.top) : mParent.getTop();
        } catch (Exception e) {
            return 0;
        }
    }

    public int getBottomLimit() {
        try {
            return (mDragRange != null && mDragRange.bottom > 0 ? Math.min(mParent.getBottom(), mDragRange.bottom) : mParent.getBottom()) - getMinHeight();
        } catch (Exception e) {
            return DisplayUtil.getMetrics().heightPixels;
        }
    }

    protected ViewDragHelper getViewDragHelper() {
        if (mDragHelper == null)
            synchronized (this) {
                if (mDragHelper == null) {
                    final float density = getResources().getDisplayMetrics().density;
                    final float minVel = MIN_FLING_VELOCITY * density;
                    mDragHelper = ViewDragHelper.create(
                            (ViewGroup) getParent(),
                            TOUCH_SLOP_SENSITIVITY,
                            callback
                    );
                    mDragHelper.setMinVelocity(minVel);
                }
            }
        return mDragHelper;
    }

    protected Rect getVisibleRect() {
        Rect rect = new Rect();
        getRootView().getWindowVisibleDisplayFrame(rect);
        return rect;
    }

    private ViewDragHelper.Callback callback = new ViewDragHelper.Callback() {

        @Override
        public boolean tryCaptureView(View child, int pointerId) {
            return child == getView() && pointerId == 0;
        }

        @Override
        public int getViewVerticalDragRange(View child) {
            return getBottomLimit() - getTopLimit();
        }

        @Override
        public int clampViewPositionVertical(View child, int top, int dy) {
            if (child == getView() && isAllowDrag()) {
                return getFixedTop(top);
            }
            return child.getTop();
        }

        @Override
        public void onViewPositionChanged(View changedView, int left, int top, int dx, int dy) {
            if (changedView == getView()) {
                onViewTransform(computePercent());
            }
        }

        @Override
        public void onViewReleased(View releasedChild, float xvel, float yvel) {
            super.onViewReleased(releasedChild, xvel, yvel);
            if (releasedChild == getView()) {
                float percent = computePercent();
                if (yvel < 0 && yvel < -mMaxFlingVelocity)
                    yvel = -mMaxFlingVelocity;
                else if (yvel > 0 && yvel > mMaxFlingVelocity) {
                    yvel = mMaxFlingVelocity;
                }
                if (onDragRelease(percent, yvel))
                    return;
                if (Math.abs(yvel) > mDragHelper.getMinVelocity()) {
                    mDragHelper.flingCapturedView(
                            getLeft(),
                            getTopLimit(),
                            getLeft(),
                            getBottomLimit());
                    postSettling();
                }
            }
        }

    };

    @Override
    protected void onFinishInflate() {
        super.onFinishInflate();

        int count = getChildCount();
        for (int i = 0; i < count; i++) {
            View child = getChildAt(i);
            if (DragUtils.isScrollerView(child) && !mScrollableViews.contains(child)) {
                mScrollableViews.add(child);
            }
        }

    }

    public void addScrollableView(View v) {
        if (v != null && !mScrollableViews.contains(v)) {
            mScrollableViews.add(v);
        }
    }

    public void removeScrollableView(View v) {
        if (v != null)
            mScrollableViews.remove(v);
    }

    @Override
    protected void onAttachedToWindow() {
        super.onAttachedToWindow();
    }

    @Override
    protected void onDetachedFromWindow() {
        mScrollableViews.clear();
        super.onDetachedFromWindow();
    }


    public View getVisibleScrollerView() {
        Rect rect = new Rect();
        for (View view : mScrollableViews) {
            view.getGlobalVisibleRect(rect);
            if (rect.width() > 0 && rect.height() > 0)
                return view;
        }
        return null;
    }

    public boolean isShowing() {
        return mParent != null && getTop() < mParent.getBottom();
    }

    public boolean isExpanded() {
        return getTop() < getBottomLimit();
    }


    public void closeAnyOther() {
        getParentView().closeAnyOther(this);
    }

    public boolean isPaused() {
        return isPaused;
    }


    public void pause() {
        if (isShowing()) {
            this.mLastTop = getTop();
            smoothTo(mParent.getBottom());
            isPaused = true;
        }
    }

    public void resume() {
        if (this.mLastTop >= 0 && this.mLastTop != getTop()) {
            show(mLastTop);
        }
    }

    public void setPaused(boolean paused) {
        isPaused = paused;
    }

    public void hide() {
        if (!isShowing())
            return;
        post(new Runnable() {
            @Override
            public void run() {
                smoothTo(mParent.getBottom());
            }
        });
    }

    public void show() {
        if (!isShowing())
            show(getBottomLimit());
    }

    public void show(float percent) {
        show(computePosition(percent));
    }

    public void show(int top) {
        isPaused = false;
        closeAnyOther();
        if (getTop() == top)
            return;
        smoothTo(top);
    }

    public int getSmoothTo() {
        return mSmoothTo;
    }

    public boolean isAvailable() {
        return isEnabled() && mSmoothTo >= getTopLimit() && mSmoothTo <= getBottomLimit();
    }

    public void smoothTo(int top) {
        isPaused = false;
        mSmoothTo = top;
        post(new Runnable() {
            @Override
            public void run() {
                if (mParent == null)
                    return;
                mDragHelper.abort();
                mDragHelper.smoothSlideViewTo(getView(), getLeft(), top);
                postSettling();

                for (OnStateChangedListener listener : mOnStateChangedListeners) {
                    listener.onPopup();
                }
            }
        });
    }

    public void expand() {
        post(new Runnable() {
            @Override
            public void run() {
                if (!isExpanded()) {
                    smoothTo(getTopLimit());
                }
            }
        });
    }

    public void collapse() {
        post(new Runnable() {
            @Override
            public void run() {
                if (isExpanded()) {
                    smoothTo(getBottomLimit());
                }
            }
        });
    }

    private int getFixedTop(int top) {
        top = Math.max(top, getTopLimit());
        top = Math.min(top, getBottomLimit());
        return top;
    }

    public float getPercent() {
        return this.percent;
    }

    public float computePercent() {
        float percent = 1 - 1.0f * (getTop() - getTopLimit()) / (getBottomLimit() - getTopLimit());
        return percent;
    }

    public int computePosition(float percent) {
        if (percent < 0 || percent > 1) {
            return getTop();
        }
        int top = (int) (getTopLimit() + (getBottomLimit() - getTopLimit()) * (1 - percent));
        return top;
    }

    protected synchronized void onViewTransform(float percent) {
        int top = getTop();
        if (this.percent == -1F || this.percent != percent) {
            final int distance = (mDragRange != null && mDragRange.bottom > 0 ? mDragRange.bottom : getBottomLimit() + getMinHeight()) - top;
            final boolean isUpSliding = this.percent > percent;
            this.percent = percent;
            for (OnScrollChangeListener listener : mOnScrollChangeListeners) {
                listener.onScroll(mDragHelper.getViewDragState() == ViewDragHelper.STATE_DRAGGING, percent, Math.max(distance, 0), isUpSliding);
            }
            if (top == mParent.getBottom()) {
                for (OnStateChangedListener listener : mOnStateChangedListeners) {
                    listener.onDismiss();
                }
            }
            getParentView().sendAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED);
            getParentView().invalidate();
        }
    }

    public void addOnStateChangedListener(OnStateChangedListener mOnDismissListener) {
        this.mOnStateChangedListeners.add(mOnDismissListener);
    }

    public void addOnScrollingListener(OnScrollChangeListener onScrollChangeListener) {
        if (onScrollChangeListener != null)
            this.mOnScrollChangeListeners.add(onScrollChangeListener);
    }

    public interface OnStateChangedListener {
        void onDismiss();

        void onPopup();
    }

    public interface OnScrollChangeListener {
        void onScroll(boolean isUser, final float percent, final float distanceY, boolean isUpSliding);
    }

    public void postSettling() {
        ViewCompat.postInvalidateOnAnimation(getParentView());
    }

}
